React上のSVGで円形のゲージを作る
SVG で円形のゲージを作ってみます。
デモ
あまり使いやすくはないのですが、簡単にデモサイトもつくってあります。コードは GitHub で公開しています。
前提となる SVG の基本知識のおさらい
1. stroke-align
下記は Figma の画面をキャプチャしたものになります。
inset | outset | center |
---|---|---|
Figma ではラインを引いて、そのラインを太くしようとしたときに、どちらの向きに太さをもたせるかといった設定ができます。
この設定を SVG の属性で再現するには stroke-align
が最適です。
なのですが、この実装が入っているブラウザは残念ですが今のところありません(2021 年 10 月 31 日現在)。
そのため、単純に 100px * 100px
の svg 上に直径 100px
、stroke-width
が 10px
の円を描くと次のように円のアウトラインが欠けた状態となります。
円の半径を outerR とすると、<circle />
に指定するべき r
は outerR - stroke-width / 2
となり、上記の円を欠けない円にするには <circle r="45" />
とする必要があります。
2. stroke-dasharray
stroke-dasharray
については上記の MDN のサイトがとてもわかりやすいです。名前の通り、stroke
の破線とその間隔を指定する属性になります。
Adobe Illustrator などでは破線の線のある部分を 線分(dash)、線のない部分を間隔(gap)としているため以降の説明に使用していきます。
stroke-dasharray
は繰り返しのこの線分と間隔を指定できるプロパティです。以下にいくつかの具体例を画像にしています。例のそれぞれの線全体の長さは 300px
です。
見ていただけるとわかるように、例えばただ 10
を指定すると線分と間隔を 10px
で繰り返す破線を描画することになります。これは 10 10
のような指定と等価になります。
連続した数値をしている例でわかりやすいのが 10 20 30
のような指定で、最初の線分が 10
、間隔が 20
、次の線分が 30
でその次の間隔が 10
と線分と間隔を繰り返すような破線になります。
今の時点ではあまり意味のある指定にはなっていませんが、線の長さと同じ数値を入れた 300
の指定されたものは指定なしと同じ表示になります。ただし見えてはいない範囲には指定なしとは異なり、両サイドに 300px
の間隔が存在しています。この間隔が次の章の stroke-dashoffset
と合わせることで面白い効果を生み出すことになります。
3. stroke-dashoffset
stroke-dashoffset
は stroke-dasharray
のオフセットを設定するものです。負の値も指定できます。
線分の開始位置がずれる、以下のように考えるとわかりやすいです。薄い灰色の枠内が通常の表示部分とすると、offset に正の値を指定すると青い枠で囲われている部分が実際に表示される部分です。
上記の例ではなかなか使い所の想像がつきにくいのですが、以下の画像を見てもらえるとわかりやすいです。
このように、<line />
の長さを l
として、その l
の value %
の長さの線は、stroke-dasharray
を l
、stroke-dashoffset
を l * (100 - value) / 100
とすることで表現できます。
円の線が描画される起点
<circle />
の stroke の起点は以下の図の赤いまるで囲われた部分です。
これは仕様で定義されています。
The arc of a ‘circle’ element begins at the "3 o'clock" point on the radius and progresses towards the "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform in the same manner as the geometry of the element.
円の起点は上にあってほしいので、これを調整するために transform
を使って回転させる必要があります。
回転は指定なしだと原点座標を基準に回転するため、円の中心で回転するように指定する必要があります。今回は円なので、半径の長さを指定することになります。
円形ゲージを作る
以上の前提をふまえると、円のゲージを作ることはそこまで難しくありません。
import { SVGAttributes, useMemo } from "react"; type Props = Readonly<{ color: SVGAttributes<SVGCircleElement>["stroke"]; r: number; strokeWidth: number; value: number; }>; export const Circle = (props: Props) => { const { color, r: outerR, strokeWidth, value } = props; /** * SVGのwidthとheightとなるサイズ */ const size = useMemo(() => { return outerR * 2; }, [outerR]); /** * strokeWidthを考慮した半径 */ const r = useMemo(() => { return outerR - strokeWidth / 2; }, [outerR, strokeWidth]); /** * 円周 */ const circumference = useMemo(() => { return 2 * Math.PI * r; }, [r]); /** * 表示する円周の長さ */ const dashoffset = useMemo(() => { return circumference * ((100 - value) / 100); }, [circumference, value]); return ( <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} > <circle r={r} cx={outerR} cy={outerR} stroke={color} fill="transparent" strokeWidth={strokeWidth} strokeDasharray={circumference} strokeDashoffset={dashoffset} transform={`rotate(-90) ${outerR} ${outerR}`} /> </svg> ); };
Props
r: outerR
は円の半径です。Props の指定で円のもっとも外側のラインを含めた半径のサイズを指定します。例えばここに 50
と指定すると縦と横が 100px の SVG に欠けることのない円がぴったり表示されることになります。
strokeWidth
はそのまま <circle />
の strokeWidth
が入ります。ただし単位つきだとうまく計算できない場合もあるので、ここでは数値のみ指定できるようにしています。
value
には円のゲージがどこまで伸長するかを割合で指定します。
コンポーネント内で行っている計算
size
は SVG の縦と横の長さになります。これは円の直径と同じサイズになるため、outerR * 2
で計算しています。
r
は半径です。r: outerR
の半径とは異なり stroke-align
を指定できないことを考慮した半径になります。
circumference
は円周です。円周は 2 * Math.PI * r
で求めることができます。ここで使用する半径は実際の線の r
になります。
dashoffset
はどこまでゲージを伸ばすかを strokeDashoffset
に指定する値となります。この値が circumference * ((100 - props.value) / 100)
で求められることは stroke-dashoffset
の項目で確認できています。
stroke-dashoffset
を使わずに作る
円形ゲージの伸長を表現するために stroke-dashoffset
を使いました。アニメーションなどを作るときにもよく使う手法なのですぐに思いついたのはこの方法でした。
ですが円形ゲージを作るために stroke-dashoffset
が必須ではありません。stroke-dasharray
を使って線分と間隔の比率を調整することで、同じことができます。
私はこの方法が思いつかず、以下のサイトを参考にさせていただきました。
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}> <circle r={r} cx={outerR} cy={outerR} stroke={color} fill="transparent" strokeWidth={strokeWidth} strokeDasharray={`${circumference * (value / 100)} ${circumference}`} transform={`rotate(-90 ${outerR} ${outerR})`} /> </svg>
こちらのほうが stroke-dashoffset
を使うよりもシンプルでわかりやすいですね。